package ru.payment.wm.impl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import ru.payment.wm.Signer;
import ru.payment.wm.UrlProvider;
import ru.payment.wm.WebMoneyException;
import ru.payment.wm.WebMoneyResponseBuilder;
import ru.payment.wm.WebMoneyService;
import ru.payment.wm.X10Response;
import ru.payment.wm.X11Response;
import ru.payment.wm.X13Response;
import ru.payment.wm.X14Response;
import ru.payment.wm.X15ListResponse;
import ru.payment.wm.X15SaveResponse;
import ru.payment.wm.X1Response;
import ru.payment.wm.X2Response;
import ru.payment.wm.X3Response;
import ru.payment.wm.X4Response;
import ru.payment.wm.X5Response;
import ru.payment.wm.X6Response;
import ru.payment.wm.X7Response;
import ru.payment.wm.X8Response;
import ru.payment.wm.X9Response;

/**
 *
 * User: sergey
 * Date: 24.03.2009
 * Time: 15:00:29
 *
 *
 */
public abstract class AbstractWebMoneyService implements WebMoneyService {

  protected final Log logger = LogFactory.getLog(getClass());

  private OutputFormat _prettyFormat = OutputFormat.createPrettyPrint();
  private SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
  private UrlProvider urlProvider;

  private SAXReader saxReader = new SAXReader();
  private HttpClient httpclient = new DefaultHttpClient();
  private WebMoneyResponseBuilder responseBuilder = new DefaultResponseBuilder();

  protected abstract boolean isSignerEnabled();
  
  public abstract Signer getSigner();

  public UrlProvider getUrlProvider() {
    return urlProvider;
  }

  public void setUrlProvider(UrlProvider urlProvider) {
    this.urlProvider = urlProvider;
  }

  public WebMoneyResponseBuilder getResponseBuilder() {
    if (responseBuilder == null) {
      throw new NullPointerException("WebMoneyResponseBuilder is not defined");
    }
    return responseBuilder;
  }

  public void setResponseBuilder(WebMoneyResponseBuilder responseBuilder) {
    this.responseBuilder = responseBuilder;
  }

  protected Document createRequest() {
    Document doc = DocumentHelper.createDocument();
    //doc.setXMLEncoding("windows-1251");
    doc.setXMLEncoding("utf-8");
    doc.addElement("w3s.request");
    return doc;
  }

  protected HttpClient getHttpClient() {
    return httpclient;
  }

  protected abstract void onBeforeRequest() throws WebMoneyException;

  protected Document sendRequest(String key, Document requestDocument) throws WebMoneyException {
    String url = getUrlProvider().get(key);

    if (logger.isDebugEnabled()) {
      logger.debug("Send xml-request to "+key+" interface : "+url);
      logger.debug(xmlAsString(requestDocument));
    }

    onBeforeRequest();

    Document docResponse = null;

    try {

      HttpPost httpPost = new HttpPost(url);
      httpPost.setEntity(new StringEntity(requestDocument.asXML(), "utf-8"));

      HttpResponse response = httpclient.execute(httpPost);
      HttpEntity entity = response.getEntity();

      docResponse = saxReader.read(entity.getContent(), "utf-8");

      if (logger.isDebugEnabled()) {
        logger.debug(xmlAsString(docResponse));
      }

      if (entity != null) {
        EntityUtils.consume (entity);
      }

      //httpclient.getConnectionManager().shutdown();

    } catch (Exception e) {
      throw new CommunicationException(e);
    }

    return docResponse;
  }

  public void httpShutdown() {
    httpclient.getConnectionManager().shutdown();
  }

  protected String xmlAsString(Document xmlDoc) {

    try {

      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      XMLWriter writer = new XMLWriter(baos, _prettyFormat);
      writer.write(xmlDoc);
      writer.close();

      return baos.toString();

    } catch (IOException ex) {
      return "XML processing error. " + ex.getMessage();
    }

  }

  /**
   * Обрезать строку если её длина более указанного числа символов
   *
   * @param str исходная строка
   * @return откорректированная строка
   */
  protected String checkLength(String str, int len) {
    
    if (str == null) {
      return "";
    }

    str = str.trim();

    if (str.length() > len) {
      str = str.substring(0, len-3)+"...";
    }

    return str;
  }

  protected String format(Date dt) {
    return sdf.format(dt);
  }


  public X1Response invokeX1(String reqNum, String reqWmId, long orderId, String customerWmId, String storePurse, float amount, String desc, String addr, int period, int exp) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    desc = checkLength(desc, 255);
    addr = checkLength(addr, 255);

    // См. "http://wiki.webmoney.ru/wiki/show/Интерфейс+X1": "незначащие нули в конце и точка, если число целое, должны отсутствовать".
    String amountStr = Math.floor(amount) == amount ? Integer.toString((int) amount) : Float.toString(amount);

    //подпись запроса формируется из параметров:
    // orderid + customerwmid + storepurse + amount + desc +
    // address + period + expiration + reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(orderId+customerWmId+storePurse+amountStr+desc+addr+period+exp+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eInv = eRoot.addElement("invoice");
    eInv.addElement("orderid").setText(String.valueOf(orderId));
    eInv.addElement("customerwmid").setText(customerWmId);
    eInv.addElement("storepurse").setText(storePurse);
    eInv.addElement("amount").setText(amountStr);
    eInv.addElement("desc").setText(desc);
    eInv.addElement("address").setText(addr);
    eInv.addElement("period").setText(String.valueOf(period));
    eInv.addElement("expiration").setText(String.valueOf(exp));

    return getResponseBuilder().buildX1Response(sendRequest(UrlProvider.X1, doc));
  }

  public X2Response invokeX2(String reqNum, String reqWmId, long tranId, String purseSrc, String purseDest, float amount, int period, String pcode, String desc, long wmInvoiceId) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    pcode = checkLength(pcode, 255);
    desc = checkLength(desc, 255);

    // Чтобы подпись совпадала, amount нужно передавать без ".0" на конце.
    // Иначе получаем ошибку -14, "signature is incorrect".
    // См. "http://wiki.webmoney.ru/wiki/show/Интерфейс+X2": "незначащие нули в конце и точка, если число целое, должны отсутствовать".
    String amountStr = Math.floor(amount) == amount ? Integer.toString((int) amount) : Float.toString(amount);

    // reqn + tranid + pursesrc + pursedest + amount +period + pcode + desc + wminvid
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(reqNum+tranId+purseSrc+purseDest+amountStr+period+pcode+desc+wmInvoiceId);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTrans = eRoot.addElement("trans");
    eTrans.addElement("tranid").setText(String.valueOf(tranId));
    eTrans.addElement("pursesrc").setText(purseSrc);
    eTrans.addElement("pursedest").setText(purseDest);
    eTrans.addElement("amount").setText(amountStr);
    eTrans.addElement("period").setText(String.valueOf(period));
    eTrans.addElement("pcode").setText(pcode);
    eTrans.addElement("desc").setText(desc);
    eTrans.addElement("wminvid").setText(String.valueOf(wmInvoiceId));

    return getResponseBuilder().buildX2Response(sendRequest(UrlProvider.X2, doc));
  }

  public X3Response invokeX3(String reqNum, String reqWmId, String purse, long wmTranId, long tranId, long wmInvoiceId, long orderId, Date dateStart, Date dateFinish) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // purse + reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(purse+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eOper = eRoot.addElement("getoperations");
    eOper.addElement("purse").setText(purse);

    if (wmTranId > 0) {
      eOper.addElement("wmtranid").setText(String.valueOf(wmTranId));
    }
    if (tranId > 0) {
      eOper.addElement("tranid").setText(String.valueOf(tranId));
    }
    if (wmInvoiceId > 0) {
      eOper.addElement("wminvid").setText(String.valueOf(wmInvoiceId));
    }
    if (orderId > 0) {
      eOper.addElement("orderid").setText(String.valueOf(orderId));
    }

    eOper.addElement("datestart").setText(format(dateStart));
    eOper.addElement("datefinish").setText(format(dateFinish));

    return getResponseBuilder().buildX3Response(sendRequest(UrlProvider.X3, doc));
  }


  public X4Response invokeX4(String reqNum, String reqWmId, String purse, long wmInvoiceId, long orderId, Date dateStart, Date dateFinish) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // purse + reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(purse+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eInv = eRoot.addElement("getoutinvoices");
    eInv.addElement("purse").setText(purse);

    if (wmInvoiceId > 0) {
      eInv.addElement("wminvid").setText(String.valueOf(wmInvoiceId));
    }
    if (orderId > 0) {
      eInv.addElement("orderid").setText(String.valueOf(orderId));
    }

    eInv.addElement("datestart").setText(format(dateStart));
    eInv.addElement("datefinish").setText(format(dateFinish));

    return getResponseBuilder().buildX4Response(sendRequest(UrlProvider.X4, doc));
  }

  public X5Response invokeX5(String reqNum, String reqWmId, long wmTranId, String pcode) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    pcode = checkLength(pcode, 255);

    // wmtranid+pcode+reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(wmTranId+pcode+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eProt = eRoot.addElement("finishprotect");
    eProt.addElement("wmtranid").setText(String.valueOf(wmTranId));
    eProt.addElement("pcode").setText(pcode);

    return getResponseBuilder().buildX5Response(sendRequest(UrlProvider.X5, doc));
  }

  public X6Response invokeX6(String reqNum, String reqWmId, String receiverWmId, String subject, String text) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    subject = checkLength(subject, 255);
    text = checkLength(text, 1024);

    // receiverwmid+reqn+msgtext+msgsubj
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(receiverWmId+reqNum+text+subject);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eMsg = eRoot.addElement("message");
    eMsg.addElement("receiverwmid").setText(receiverWmId);
    eMsg.addElement("msgsubj").setText(subject);
    eMsg.addElement("msgtext").setText(text);

    return getResponseBuilder().buildX6Response(sendRequest(UrlProvider.X6, doc));
  }

  public X7Response invokeX7(String reqWmId, String authWmId, String sign, String plan) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // wmid+testsign\wmid+testsign\plan+testsign\sign
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String reqSign = getSigner().sign(reqWmId+authWmId+plan+sign);
      eRoot.addElement("sign").setText(reqSign);
    }

    Element eTest = eRoot.addElement("testsign");
    eTest.addElement("wmid").setText(authWmId);
    eTest.addElement("plan").setText(plan);
    eTest.addElement("sign").setText(sign);

    return getResponseBuilder().buildX7Response(sendRequest(UrlProvider.X7, doc));
  }

  public X8Response invokeX8(String reqNum, String reqWmId, String findWmId, String findPurse) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // testwmpurse\wmid+testwmpurse\purse
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(findWmId+findPurse);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTest = eRoot.addElement("testwmpurse");
    eTest.addElement("wmid").setText(findWmId);
    eTest.addElement("purse").setText(findPurse);

    return getResponseBuilder().buildX8Response(sendRequest(UrlProvider.X8, doc));
  }

  public X9Response invokeX9(String reqNum, String reqWmId, String infoWmId) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // getpurses/wmid+reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(infoWmId+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eGet = eRoot.addElement("getpurses");
    eGet.addElement("wmid").setText(infoWmId);

    return getResponseBuilder().buildX9Response(sendRequest(UrlProvider.X9, doc));
  }

  public X10Response invokeX10(String reqNum, String reqWmId, String wmId, long wmInvoiceId, Date dateStart, Date dateFinish) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    String szDtStart = format(dateStart);
    String szDtFin = format(dateFinish);

    // getininvoices/wmid + getininvoices/wminvid + getininvoices/datestart + getininvoices/datefinish + reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(wmId+wmInvoiceId+szDtStart+szDtFin+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eGet = eRoot.addElement("getininvoices");
    eGet.addElement("wmid").setText(wmId);
    if (wmInvoiceId > 0) {
      eGet.addElement("wminvid").setText(String.valueOf(wmInvoiceId));
    }
    eGet.addElement("datestart").setText(szDtStart);
    eGet.addElement("datefinish").setText(szDtFin);

    return getResponseBuilder().buildX10Response(sendRequest(UrlProvider.X10, doc));
  }

  public X11Response invokeX11(String reqWmId, String passportWmId, boolean dict, boolean info, boolean mode) throws WebMoneyException {
    Document doc = DocumentHelper.createDocument();
    doc.setXMLEncoding("utf-8");
    doc.addElement("request");
    Element eRoot = doc.getRootElement();

    // wmid + passportwmid
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(reqWmId+passportWmId);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("passportwmid").setText(passportWmId);

    Element eParams = eRoot.addElement("params");
    eParams.addElement("dict").setText(dict?"1":"");
    eParams.addElement("info").setText(info?"1":"");
    eParams.addElement("mode").setText(mode?"1":"");

    return getResponseBuilder().buildX11Response(sendRequest(UrlProvider.X11, doc));
  }

  public X13Response invokeX13(String reqNum, String reqWmId, long wmTranId) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // rejectprotect/wmtranid+reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(wmTranId + reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eRP = eRoot.addElement("rejectprotect");
    eRP.addElement("wmtranid").setText(String.valueOf(wmTranId));

    return getResponseBuilder().buildX13Response(sendRequest(UrlProvider.X13, doc));
  }

  public X14Response invokeX14(String reqNum, String reqWmId, long wmTranId, float amount) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // Отсутствие ".0" на конце может быть важным для срабатывания подписи.
    String amountStr = Math.floor(amount) == amount ? Integer.toString((int) amount) : Float.toString(amount);

    // reqn+trans/inwmtranid+trans/amount
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(reqNum+wmTranId+amountStr);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTrans = eRoot.addElement("trans");
    eTrans.addElement("inwmtranid").setText(String.valueOf(wmTranId));
    eTrans.addElement("amount").setText(amountStr);

    return getResponseBuilder().buildX14Response(sendRequest(UrlProvider.X14, doc));
  }

  public X15ListResponse invokeX15List(String reqNum, String reqWmId, String trustWmId) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    eRoot.addElement("wmid").setText(reqWmId);

    // gettrustlist\wmid+reqn
    if (isSignerEnabled()) {
      String sign = getSigner().sign(trustWmId+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTrust = eRoot.addElement("gettrustlist");
    eTrust.addElement("wmid").setText(trustWmId);

    return getResponseBuilder().buildX15Response(sendRequest(UrlProvider.X15List, doc));
  }

  public X15ListResponse invokeX15List2(String reqNum, String reqWmId, String trustWmId) throws WebMoneyException {
    if (!trustWmId.equals(reqWmId)) {
      throw new ProtocolException("wmId and trustWmId must be different."); 
    }

    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(trustWmId+reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTrust = eRoot.addElement("gettrustlist");
    eTrust.addElement("wmid").setText(trustWmId);

    return getResponseBuilder().buildX15Response(sendRequest(UrlProvider.X15List2, doc));
  }

  public X15SaveResponse invokeX15Save(String reqNum, String reqWmId, boolean allowInvoice, boolean allowTrans, boolean allowPurse, boolean allowTransHist,
                               String masterWmId, String slaveWmId, String purse, float limit, float dayLimit, float weekLimit, float monthLimit) throws WebMoneyException {
    Document doc = createRequest();
    Element eRoot = doc.getRootElement();

    // wmid + trust\purse + trust\masterwmid + reqn
    if (isSignerEnabled()) {
      eRoot.addElement("wmid").setText(reqWmId);
      String sign = getSigner().sign(reqWmId + purse + masterWmId + reqNum);
      eRoot.addElement("sign").setText(sign);
    }

    eRoot.addElement("reqn").setText(reqNum);

    Element eTrust = eRoot.addElement("trust").
            addAttribute("inv", allowInvoice?"1":"0").
            addAttribute("trans", allowTrans?"1":"0").
            addAttribute("purse", allowPurse?"1":"0").
            addAttribute("transhist", allowTransHist?"1":"0");
    eTrust.addElement("masterwmid").setText(masterWmId);
    eTrust.addElement("slavewmid").setText(slaveWmId);
    eTrust.addElement("purse").setText(purse);
    eTrust.addElement("limit").setText(String.valueOf(limit));
    eTrust.addElement("daylimit").setText(String.valueOf(dayLimit));
    eTrust.addElement("weeklimit").setText(String.valueOf(weekLimit));
    eTrust.addElement("monthlimit").setText(String.valueOf(monthLimit));

    return getResponseBuilder().buildX15SaveResponse(sendRequest(UrlProvider.X15Save2, doc));
  }

}
